/*
 * DG (DG Gaming Engine)  - Server
 * Copyright (C) 2004  Bob Marks (marksie531@yahoo.com)
 * http://code.google.com/p/dng-gaming
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 */
package com.dg.gaming.server;

import java.io.*;
import java.nio.*;
import java.util.*;

import com.dg.gaming.api.common.*;
import com.dg.gaming.api.common.msg.*;
import com.dg.gaming.api.common.playerstate.*;
import com.dg.gaming.api.common.util.*;
import com.dg.gaming.server.controllers.*;
import com.dg.gaming.server.data.*;
import javolution.util.*;
import nanoxml.*;
import org.xsocket.connection.*;

/**
 * <p>Every time a client connections to a DGServer this class (which extends
 * Thread) is run.  This ensures that a DGServer is multi-threaded.</p>
 *
 * @author  Bob Marks
 * @version Beta 0.3
 * @see com.dg.gaming.server.data.DGServer
 */
public class ServerConnectionThread extends AbstractConnectionThread implements
        IGame {

    /** Logging */
    DGLogger logger = new DGLogger(this.getClass());

    /** link to the DGServer */
    protected GameServer server;

    /** Link to the various parsers. */
    private ServerControllerList controllers = null;

    /** Game ID which is set to a particular game, master server or admin. */
    private String gameID = null;

    /** Convience link to the userlist. */
    private UserList userList = null;

    /** Convience link to the tablelist. */
    private TableList tableList = null;

    /** Convience link to the game. */
    private Game game = null;

    /** Connection list. */
    private ConnectionList connections = null;

    private boolean isAdministrator = false;

    /**
     * Constructor which takes a socket connection and a link to the server.
     *
     * @param clientSocket      Client connection.
     */

    private INonBlockingConnection uo = null;


    private static final byte STATE_NOT_CONNECTED = 0;
    private static final byte STATE_CONNECTED = 1;
    private static final byte STATE_PART_DISCONNECTED = 0;


    private byte state = STATE_NOT_CONNECTED;


    private List<XMLElement> list = Collections.synchronizedList(new
            FastList<XMLElement>());

    private String sessionID = "";

    private void userDisconnected() {
        try {
            uo.close();
        } catch (Exception ex) {

        }

    }

    public ServerConnectionThread(INonBlockingConnection uo) {
        super();
        this.uo = uo;
        state = STATE_NOT_CONNECTED;
        // Get link to the server (singleton) and the various parsers
        this.server = GameServer.getInstance();
        this.controllers = server.getControllers();

        // and also set up links to server objects which are often used.
        this.connections = server.getConnections(); // link to connections
    }

    /**
     * Initilise this server connection thread with a username and a
     * game key.
     *
     * @param username  Username of the client connecting to this thread.
     * @param gameID    Gamekey of the client connecting.
     */
    public void init(long userid, String gameID) {
        this.userid = userid;
        this.gameID = gameID;

        // Set convience fields
        this.game = server.getGameList().getGame(gameID);
        this.userList = game.getUserList(); // link to userlist
        this.tableList = game.getTableList(); // link to table list
//        Worker.addConnection(this);
        state = STATE_CONNECTED;
    }

    public String getId() {
        return uo.getId();
    }

    /**
     * Initilise this server connection thread for an administrator user.
     *
     * @param admin_username   Administrator username.
     */
    public void init(long admin_userId) {

        this.userid = admin_userId;
        this.gameID = ADMINISTRATOR_GAME;
        this.isAdministrator = true;
//        Worker.addConnection(this);
        state = STATE_CONNECTED;
    }

    /**
     * Implementation of the parse method on the server side.  This method
     * recieves XML communication from a client.
     *
     * @see ocom.dg.gaming.api.common.AbstractConnectionThread#parse (XMLElement element)
     */
    public void parse(XMLElement message) throws TransmissionException {
        if (message != null) {
            // Retrieve table number
            String sTableNum = message.getStringAttribute(MsgTableMessage.
                    XML_ATT_TABLE_NUM);

            // If message contains a "table" attribute then delegate to a table message
            if (sTableNum == null) {
                // Parse game message
                controllers.getGameController().parseGameMessage(this, message);
            } else {
                String tableNum = sTableNum;
                controllers.getTableController().parseTableMessage(this,
                        message, tableNum);
            }

            // Send a copy to the administrator thread if it is listening.
            sendGameMessageToAdmin(message, true);
        }
    }

    public void serverSend(ITransmittable transObject) {
        send(transObject, true);
    }


    public void send(ITransmittable transObject) {
        // Retrieve XMLElement from the object and flatten to a String.
        String message = transObject.flatten().toString();

        // Send down the socket to the receiving end
        try {
            long userID = this.userid;
            uo.write((message + "\n").getBytes("UTF8"));
            uo.flush();
        } catch (BufferOverflowException ex) {
        } catch (IOException ex) {
        }
    }

    /**
     * Send a ITransmittable object to the output stream (could be server or
     * client).
     *
     * @param transObject
     * @param sendCopyToAdmin   If this is true then a copy is sent to the administrator.
     */
    protected void send(ITransmittable transObject, boolean sendCopyToAdmin) {
        send(transObject);

        // Send to administrator if the administrator is listening.
        if (sendCopyToAdmin) {
            sendGameMessageToAdmin(transObject.flatten(), false);
        }
    }

    /**
     * Send game message to administration connection.  The game message will
     * be wrapped in in a CommAdminGameMessage.
     *
     * @param message             Game message as an XML message.
     * @param isReceivingMessage  If true then going to
     */
    public void sendGameMessageToAdmin(XMLElement message,
                                       boolean isReceivingMessage) {
        // Wrap the message inside an <admin_message> which details
        // additional info about the message such as which user thread has sent it
        // and what game.
        ServerConnectionThread conn = connections.getAdminConnection();

        if (conn != null) { // ensure we're not sending same message twice
            MsgAdminGameMessage adminMessage = new MsgAdminGameMessage(
                    isReceivingMessage, gameID, userid, message);
            conn.send(adminMessage, false);
        }
    }

    /**
     * Send data update message to administration connection.  The update message
     * will be wrapped in a CommAdminDataMessage object.
     *
     * @param message
     * @param isReceivingMessage
     */
    public void sendDataMessageToAdmin(ITransmittable message) {
        ServerConnectionThread conn = connections.getAdminConnection();

        if (conn != null) { // ensure we're not sending same message twice
            MsgAdminMessage adminMessage = new MsgAdminMessage(gameID,
                    userid, message.flatten());
            conn.send(adminMessage, false);
        }
    }

    /**
     * Convience method to send a message - assumes that message is also being
     * sent to the admin client.
     *
     * @see ocom.dg.gaming.api.common.AbstractConnectionThread#send(ocom.dg.gaming.api.common.comm.ITransmittable)
     */
//    public void send(ITransmittable transObject) {
//        send(transObject, true);
//    }

    /**
     * Return the game object currently tied to this connection thread..
     *
     * @return   Game object (which contains the userlist and table list).
     */
    public Game getGame() {
        return game;
    }

    /**
     * Return the gameID.
     *
     * @return    Game ID (e.g. chess-0.2).
     */
    public String getGameID() {
        return this.gameID;
    }

    /**
     * Return the user list for this game.
     *
     * @return   Userlist.
     */
    public UserList getUserList() {
        return this.userList;
    }

    /**
     * Return the table list.
     *
     * @return
     */
    public TableList getTableList() {
        return this.tableList;
    }

    /**
     * Return true/false if this user is administrator.
     *
     * @return  True if user is an administrator.
     */
    public boolean isAdministrator() {
        return this.isAdministrator;
    }

    /**
     * Client has exitted so clean everything up.
     *
     * @see ocom.dg.gaming.api.common.AbstractConnectionThread#cleanup()
     */
    public void cleanup() {
        logger.debug("cleanup", "Cleaning up user details");

        // Remove connection from the connection list (game & admin users)
        if (isAdministrator()) {
            server.getConnections().removeAdminConnection();
        } else {
            server.getConnections().removeConnection(gameID, userid);
        }

        // Game users only
        if (userList != null && tableList != null) {
            logger.debug("cleanup", "remove user");
            userList.removeUser(userid);

            // remove any instance of the user from the table list
            logger.debug("cleanup", "removing table");
            tableList.removeUserFromTables(userid);

            // Inform all connected clients that user has disconnected from game
            MsgDisconnect commDisconnect = new MsgDisconnect(userid);
            broadcast(commDisconnect);
            sendDataMessageToAdmin(commDisconnect);

            // Log message
            logger.log("DG Games Server: client [" + userid +
                       "] has logged off");

            // Update snapshot
            try {
                server.getServerData().updateSnapshot(
                        getGameID(), getUserList().size(), getTableList().size());
            } catch (ServerDataException e) {
                e.printStackTrace(); // proper logging at some stage
            }
        }
    }

    /**
     * Transmit a message to the specified table and specified player.
     *
     * @param usernameTo Username to transmit to
     * @param tableNum Table number to transmit to
     * @param transObject Transmittable object
     */
    public void transmitToTablePlayer(long usernameTo, String tableNum,
                                      ITransmittable transObject) {
        if (transObject instanceof MsgTableMessage) {
            ((MsgTableMessage) transObject).setTableNum(tableNum);
        }

        // Insure that that user is actually in this table
        Table table = tableList.getTable(tableNum);
        if (table != null) {
            PlayerList playerList = table.getPlayerList();
            Player player = playerList.getPlayer(usernameTo);
            if (player != null) {
                transmit(usernameTo, transObject);
            }
        }
    }

    /**
     * Transmit a message to the specified table.
     *
     * @param tableNum
     * @param transObject
     */
    public void transmitToTablePlayers(String tableNum,
                                       ITransmittable transObject) {
        if (transObject instanceof MsgTableMessage) {
            ((MsgTableMessage) transObject).setTableNum(tableNum);
        }

        Table table = tableList.getTable(tableNum);
        if (table != null) {
            Vector players = table.getPlayerList().getPlayers();

            for (int i = 0; i < players.size(); i++) {
                Player player = (Player) players.get(i);
                long username = player.getPlayerId();
                transmit(username, transObject);
            }
        }
    }

    /**
     * Transmit a messgae to a table but omit a user (this user will generally
     * be the player who has created the message in the first place).
     *
     * @param omitUser     Username to omit from the list (usually the sender).
     * @param tableNum     Table number to transmit message to.
     * @param transObject  Object which implements the ITransmittable interface.
     */
    public void transmitToTablePlayers(long omitUser, String tableNum,
                                       ITransmittable transObject) {
        transmitToTablePlayers(omitUser, tableNum, transObject, false);
    }

    /**
     * Overloaded version which only transmits to players in a game in progress i.e. transmits
     * to players whose state is "PlayerStateGameStarted".
     *
     * @param omitUser       Username to omit from the list (usually the sender).
     * @param tableNum       Table number to transmit message to.
     * @param transObject    Object which implements the ITransmittable interface.
     * @param gameInProgress If true only transmits to players if they are playing.
     */
    public void transmitToTablePlayers(long omitUser, String tableNum,
                                       ITransmittable transObject,
                                       boolean gameInProgress) {
        if (transObject instanceof MsgTableMessage) {
            ((MsgTableMessage) transObject).setTableNum(tableNum);
        }

        Table table = tableList.getTable(tableNum);
        if (table != null) {
            Vector players = table.getPlayerList().getPlayers();

            // Loop throught the various players and transmit the message
            for (int i = 0; i < players.size(); i++) {
                Player player = (Player) players.get(i);
                long username = player.getPlayerId();

                if (username != omitUser) {
                    if (gameInProgress) {
                        if (player.getState() instanceof PlayerStateGameStarted) {
                            transmit(userid, transObject);
                        }
                    } else {
                        transmit(username, transObject);
                    }
                }
            }
        }
    }

    /**
     * Broadcast a transmittable object to all the clients.
     *
     * @param transmittableObject
     */
    public void broadcast(ITransmittable transmittableObject) {
        // retrieve all the clients
        Vector users = userList.getUsers();
        for (int i = 0; i < users.size(); i++) {
            transmit((Long) users.get(i), transmittableObject);
        }
    }

    /**
     * Broadcast a transmittable object to all the clients.
     *
     * @param omitUser             Omit this user (usually the person who sent the message).
     * @param transmittableObject  Object to transmit.
     */
    public void broadcast(long omitUser, ITransmittable transmittableObject) {
        // retrieve all the clients
        Vector users = userList.getUsers();
        for (int i = 0; i < users.size(); i++) {
            long currentUsername = (Long) users.get(i);
            if (omitUser != currentUsername) {
                transmit(currentUsername, transmittableObject);
            }
        }
    }

    /**
     * Overloaded version which takes a MsgTableMessage object.
     *
     * @param username     Username to send message to.
     * @param transObject  Transmittable object.
     */
    public void transmit(long username, ITransmittable transObject) {
        ServerConnectionThread conn = connections.getServerConnectionThread(
                getGameID(), username);

        // Send the object to the user
        conn.serverSend(transObject);
    }

    /**
     * Send a ITransmittable object to user connected to this thread only.
     *
     * @param transObject   Transmittable object.
     */
    public void transmit(ITransmittable transObject) {
        // Send the object to the user
        serverSend(transObject);
    }

    /**
     * This metohd returns a server controller associated with this
     * server connection thread.
     *
     * @return   Server connection.
     */
    public ServerController getServerController() {
        return GameServer.getInstance().getControllers().getCustomController(
                gameID);
    }
}
